package com.cy.hui_jie_dan.websocket;

import com.cy.hui_jie_dan.popj.entity.ChatMessage;
import com.cy.hui_jie_dan.service.ChatMessageService;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j
@Sharable
@Component
public class NettyWebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    static {
        // Configure ObjectMapper to support Java 8 Date and Time types
        objectMapper.registerModule(new JavaTimeModule());
        // Optional: Disable writing dates as timestamps if you want ISO 8601 format
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // Optional: Configure other features if needed, e.g., ignoring unknown properties
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }
    
    // User ID to Channel mapping (online user management)
    private static final Map<Integer, Channel> CHANNEL_MAP = new ConcurrentHashMap<>();
    
    // Channel to User ID reverse mapping
    private static final Map<Channel, Integer> USER_MAP = new ConcurrentHashMap<>();
    
    // User online status management
    private static final Map<Integer, Boolean> userOnlineStatus = new ConcurrentHashMap<>();
    
    // Record user last connection time (for frequency control)
    private static final Map<Integer, Long> lastConnectionTime = new ConcurrentHashMap<>();
    
    // Minimum reconnection interval (milliseconds)
    private static final long RECONNECTION_INTERVAL = 5000;
    
    // Inactive connection cleanup interval (milliseconds)
    private static final long CLEANUP_INTERVAL = 300000;
    
    // Scheduled task executor - for periodic cleanup of inactive connections
    private static final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
    
    // Redis message prefix
    private static final String CHAT_MESSAGE_KEY_PREFIX = "chat:message:";
    private static final String CHAT_HISTORY_KEY_PREFIX = "chat:history:";
    private static final String UNREAD_COUNT_KEY_PREFIX = "chat:unread:count:";
    
    // Message expiration time (30 days)
    private static final long MESSAGE_EXPIRATION = 30 * 24 * 60 * 60; // 30 days, unit: seconds

    private static ChatMessageService chatMessageService;
    private static RedisTemplate<String, Object> redisTemplate;

    static {
        // Initialize scheduled task: periodically clean up inactive WebSocket connections
        cleanupExecutor.scheduleAtFixedRate(() -> {
            try {
                CHANNEL_MAP.entrySet().removeIf(entry -> {
                    Channel channel = entry.getValue();
                    if (!channel.isActive()) {
                        Integer userId = entry.getKey();
                        userOnlineStatus.remove(userId);
                        lastConnectionTime.remove(userId);
                        log.info("Cleaning up inactive connection: User ID={}", userId);
                        return true;
                    }
                    return false;
                });
            } catch (Exception e) {
                log.error("Connection cleanup task exception: {}", e.getMessage(), e);
            }
        }, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.MILLISECONDS);
    }

    @Autowired
    public void setChatMessageService(ChatMessageService service) {
        chatMessageService = service;
    }

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> template) {
        redisTemplate = template;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        if (!(frame instanceof TextWebSocketFrame)) {
            log.warn("Unsupported WebSocket frame type: {}", frame.getClass().getName());
            return;
        }

        String message = ((TextWebSocketFrame) frame).text();
        log.debug("Received message: {}", message);

        try {
            Map<String, Object> messageMap = objectMapper.readValue(message, Map.class);
            String type = (String) messageMap.get("type");
            
            if (type == null) {
                log.warn("Received invalid message: Missing type field");
                sendErrorResponse(ctx, "Message format error: Missing type field");
                return;
            }

            switch (type) {
                case "CONNECT":
                    handleConnect(ctx, messageMap);
                    break;
                case "CHAT":
                    handleChat(messageMap);
                    break;
                case "HEARTBEAT":
                    handleHeartbeat(ctx);
                    break;
                case "READ_ACK":
                    handleReadAck(messageMap);
                    break;
                case "REQUEST_OFFLINE":
                    handleOfflineMessagesRequest(ctx, messageMap);
                    break;
                default:
                    log.warn("Unknown message type: {}", type);
                    sendErrorResponse(ctx, "Unknown message type");
            }
        } catch (Exception e) {
            log.error("Error processing message: {}", e.getMessage(), e);
            sendErrorResponse(ctx, "Message processing failed: " + e.getMessage());
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        log.info("New WebSocket connection established: {}", ctx.channel().remoteAddress());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        Integer userId = USER_MAP.remove(channel);

        if (userId != null) {
            boolean wasThisTheActiveChannel = CHANNEL_MAP.remove(userId, channel);
            
            if (wasThisTheActiveChannel) {
                userOnlineStatus.put(userId, false);
                log.info("User {} disconnected. Channel {} removed from CHANNEL_MAP.", userId, channel.id());
                broadcastUserStatus(userId, false);
                
                // Don't immediately remove from online status to allow for reconnection
                // The status will be updated if a new connection is established
                ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
                executor.schedule(() -> {
                    if (!userOnlineStatus.getOrDefault(userId, false)) {
                        log.info("User {} has not reconnected within timeout period", userId);
                    }
                }, 30, TimeUnit.SECONDS);
                executor.shutdown();
            }
        } else {
            log.warn("Channel {} became inactive, but no userId was found in USER_MAP for it.", channel.id());
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.warn("Connection read timeout: {}", ctx.channel().remoteAddress());
                ctx.close();
            }
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("WebSocket handling exception: {}", cause.getMessage(), cause);
        ctx.close();
    }

    private void handleConnect(ChannelHandlerContext ctx, Map<String, Object> messageMap) {
        try {
            Object userIdObj = messageMap.get("userId");
            if (userIdObj == null) {
                sendErrorResponse(ctx, "Missing userId parameter");
                return;
            }

            Integer userId = Integer.parseInt(userIdObj.toString());
            log.info("Handling connection for user ID: {}", userId);
            
            // Check reconnection frequency
            Long lastConnTime = lastConnectionTime.get(userId);
            if (lastConnTime != null && System.currentTimeMillis() - lastConnTime < RECONNECTION_INTERVAL) {
                log.warn("User {} attempting to reconnect too frequently", userId);
                sendErrorResponse(ctx, "Reconnecting too frequently, please try again later");
                ctx.close();
                return;
            }

            // Handle duplicate logins
            Channel oldChannel = CHANNEL_MAP.get(userId);
            if (oldChannel != null && oldChannel.isActive()) {
                log.info("User {} already has an active connection, closing old connection", userId);
                Map<String, Object> forceDisconnectResponse = new HashMap<>();
                forceDisconnectResponse.put("type", "FORCE_DISCONNECT");
                forceDisconnectResponse.put("message", "Account logged in on another device");
                forceDisconnectResponse.put("timestamp", System.currentTimeMillis());
                forceDisconnectResponse.put("userId", userId);
                oldChannel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(forceDisconnectResponse)));
                oldChannel.close();
                
                // Remove old channel mappings
                CHANNEL_MAP.remove(userId);
                USER_MAP.remove(oldChannel);
            }

            // Establish user-channel mapping
            CHANNEL_MAP.put(userId, ctx.channel());
            USER_MAP.put(ctx.channel(), userId);
            userOnlineStatus.put(userId, true);
            lastConnectionTime.put(userId, System.currentTimeMillis());

            log.info("User {} connected successfully", userId);

            // Send connection success response
            Map<String, Object> response = new HashMap<>();
            response.put("type", "CONNECT_ACK");
            response.put("status", 1);
            response.put("userId", userId);
            ctx.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
        } catch (Exception e) {
            log.error("Error handling connection: {}", e.getMessage(), e);
            sendErrorResponse(ctx, "Internal server error");
            ctx.close();
        }
    }

    private void handleChat(Map<String, Object> messageMap) {
        try {
            // Validate required fields
            if (messageMap == null) {
                log.error("Received null message map");
                return;
            }

            Object fromUserIdObj = messageMap.get("fromUserId");
            Object toUserIdObj = messageMap.get("toUserId");
            Object contentObj = messageMap.get("content");
            Object contentTypeObj = messageMap.get("contentType");
            Object messageTypeObj = messageMap.get("messageType");

            // Check if any required field is null
            if (fromUserIdObj == null || toUserIdObj == null || contentObj == null) {
                log.error("Missing required fields in chat message. fromUserId: {}, toUserId: {}, content: {}", 
                    fromUserIdObj, toUserIdObj, contentObj);
                return;
            }

            Integer fromUserId = Integer.parseInt(fromUserIdObj.toString());
            Integer toUserId = Integer.parseInt(toUserIdObj.toString());
            String content = contentObj.toString();
            
            // Handle both contentType and messageType
            Integer contentType;
            if (contentTypeObj != null) {
                contentType = Integer.parseInt(contentTypeObj.toString());
            } else if (messageTypeObj != null) {
                // Convert messageType string to contentType integer
                contentType = "text".equals(messageTypeObj.toString()) ? 1 : 2;
            } else {
                // Default to text type if neither is specified
                contentType = 1;
            }

            // Create message object
            ChatMessage chatMessage = new ChatMessage()
                    .setFromUserId(fromUserId)
                    .setToUserId(toUserId)
                    .setContent(content)
                    .setContentType(contentType)
                    .setStatus(0) // 0 means unread
                    .setCreateTime(Instant.now())
                    .setUpdateTime(Instant.now());

            // Save message
            chatMessageService.saveMessage(chatMessage);

            // Get sender avatar (this requires querying the user table based on fromUserId, assume a userService exists)
            // To simplify, a placeholder is used here, should actually call userService.getUserById(fromUserId).getAvatarUrl()
            String senderAvatar = "/static/頭像2.jpg"; // Placeholder, needs actual query
            // TODO: Replace with actual avatar lookup

            // Build the message Map to send to the receiver, including all fields required by the frontend
            Map<String, Object> messageToSend = new HashMap<>();
            messageToSend.put("type", "CHAT");
            messageToSend.put("content", chatMessage.getContent());
            messageToSend.put("avatar", senderAvatar);
            messageToSend.put("fromUserId", chatMessage.getFromUserId());
            messageToSend.put("toUserId", chatMessage.getToUserId());
            messageToSend.put("messageId", chatMessage.getMessageId());
            messageToSend.put("messageType", chatMessage.getContentType() == 1 ? "text" : "image");
            messageToSend.put("createTime", chatMessage.getCreateTime());
            messageToSend.put("timestamp", System.currentTimeMillis());

            // Send message to receiver
            Channel targetChannel = CHANNEL_MAP.get(toUserId);
            if (targetChannel != null && targetChannel.isActive()) {
                targetChannel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(messageToSend)));
                log.info("Message sent successfully to user {}: {}", toUserId, messageToSend);
            } else {
                 log.warn("User {} is offline, message saved but not sent in real time, storing to Redis", toUserId);
                 // Store offline message to Redis
                 storeOfflineMessage(toUserId, chatMessage);
            }

        } catch (NumberFormatException e) {
            log.error("Invalid number format in chat message: {}", e.getMessage(), e);
        } catch (Exception e) {
            log.error("Error handling chat message: {}", e.getMessage(), e);
        }
    }

    private void handleHeartbeat(ChannelHandlerContext ctx) {
        try {
            Map<String, Object> response = new HashMap<>();
            response.put("type", "HEARTBEAT");
            response.put("timestamp", System.currentTimeMillis());
            ctx.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
        } catch (Exception e) {
            log.error("Error handling heartbeat: {}", e.getMessage());
        }
    }

    private void handleReadAck(Map<String, Object> messageMap) {
        try {
            Integer fromUserId = Integer.parseInt(messageMap.get("fromUserId").toString());
            Integer toUserId = Integer.parseInt(messageMap.get("toUserId").toString());
            String messageId = messageMap.get("messageId").toString();

            // Update message status to read
            chatMessageService.markMessageAsRead(toUserId, fromUserId);

            // Notify sender message is read
            Map<String, Object> ackResponse = new HashMap<>();
            ackResponse.put("type", "READ_ACK");
            ackResponse.put("messageId", messageId);
            ackResponse.put("fromUserId", fromUserId);
            ackResponse.put("timestamp", System.currentTimeMillis());

            Channel fromChannel = CHANNEL_MAP.get(fromUserId);
            if (fromChannel != null && fromChannel.isActive()) {
                fromChannel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(ackResponse)));
            }
        } catch (Exception e) {
            log.error("Error handling read acknowledgment: {}", e.getMessage());
        }
    }

    private void handleOfflineMessagesRequest(ChannelHandlerContext ctx, Map<String, Object> messageMap) {
        try {
            Object userIdObj = messageMap.get("userId");
            if (userIdObj == null) {
                sendErrorResponse(ctx, "Missing userId parameter");
                return;
            }
            Integer userId = Integer.parseInt(userIdObj.toString());
            List<ChatMessage> offlineMessages = getOfflineMessages(userId);
            
            Map<String, Object> response = new HashMap<>();
            response.put("type", "OFFLINE_MESSAGES");
            response.put("messages", offlineMessages);
            response.put("timestamp", System.currentTimeMillis());
            
            ctx.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
            log.info("Sent {} offline messages to user {}", offlineMessages.size(), userId);
        } catch (Exception e) {
            log.error("Error handling offline messages request: {}", e.getMessage(), e);
            sendErrorResponse(ctx, "Failed to retrieve offline messages");
        }
    }

    private void sendErrorResponse(ChannelHandlerContext ctx, String errorMessage) {
        try {
            Map<String, Object> errorResponse = new HashMap<>();
            errorResponse.put("type", "ERROR");
            errorResponse.put("message", errorMessage);
            errorResponse.put("timestamp", System.currentTimeMillis());
            ctx.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(errorResponse)));
        } catch (Exception e) {
            log.error("Error sending error response: {}", e.getMessage());
        }
    }

    private void sendJsonResponse(ChannelHandlerContext ctx, Map<String, Object> response) {
        try {
            ctx.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
        } catch (Exception e) {
            log.error("Error sending JSON response: {}", e.getMessage());
        }
    }

    private void broadcastUserStatus(Integer userId, boolean online) {
        try {
            Map<String, Object> statusMessage = new HashMap<>();
            statusMessage.put("type", "USER_STATUS");
            statusMessage.put("userId", userId);
            statusMessage.put("online", online);
            statusMessage.put("timestamp", System.currentTimeMillis());

            String message = objectMapper.writeValueAsString(statusMessage);
            CHANNEL_MAP.forEach((uid, channel) -> {
                if (channel.isActive() && !uid.equals(userId)) {
                    channel.writeAndFlush(new TextWebSocketFrame(message));
                }
            });
        } catch (Exception e) {
            log.error("Error broadcasting user status: {}", e.getMessage());
        }
    }

    public static void sendChatMessage(String userId, Object message) {
        try {
            Channel channel = CHANNEL_MAP.get(Integer.parseInt(userId));
            if (channel != null && channel.isActive()) {
                channel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(message)));
            }
        } catch (Exception e) {
            log.error("Error sending message: {}", e.getMessage(), e);
        }
    }

    public static boolean isUserOnline(Integer userId) {
        return userOnlineStatus.getOrDefault(userId, false);
    }

    /**
     * Store offline messages to Redis
     * @param userId Receiver user ID
     * @param message Offline message
     */
    private void storeOfflineMessage(Integer userId, ChatMessage message) {
        try {
            String key = CHAT_MESSAGE_KEY_PREFIX + "offline:" + userId;
            redisTemplate.opsForList().rightPush(key, message);
            redisTemplate.expire(key, MESSAGE_EXPIRATION, TimeUnit.SECONDS);
            log.info("Offline message stored to Redis successfully: User ID={}, Message ID={}, Content={}", 
                userId, message.getMessageId(), message.getContent());
        } catch (Exception e) {
            log.error("Failed to store offline message to Redis: User ID={}, Message ID={}", userId, message.getMessageId(), e);
        }
    }

    /**
     * Get offline messages from Redis
     * @param userId User ID
     * @return List of offline messages
     */
    private List<ChatMessage> getOfflineMessages(Integer userId) {
        List<ChatMessage> offlineMessages = new ArrayList<>();
        try {
            String key = CHAT_MESSAGE_KEY_PREFIX + "offline:" + userId;
            Long size = redisTemplate.opsForList().size(key);
            if (size != null && size > 0) {
                List<Object> messageObjects = redisTemplate.opsForList().range(key, 0, size - 1);
                if (messageObjects != null) {
                    for (Object msgObj : messageObjects) {
                        if (msgObj instanceof ChatMessage) {
                            offlineMessages.add((ChatMessage) msgObj);
                        } else if (msgObj instanceof Map) {
                            try {
                                ChatMessage chatMessage = objectMapper.convertValue(msgObj, ChatMessage.class);
                                offlineMessages.add(chatMessage);
                            } catch (IllegalArgumentException e) {
                                log.error("Failed to deserialize offline message from Redis: {}", msgObj, e);
                            }
                        }
                    }
                }
                // Clean up offline messages after sending
                redisTemplate.delete(key);
                log.info("Successfully retrieved and cleaned up offline messages from Redis: User ID={}, Message Count={}", userId, offlineMessages.size());
            } else {
                log.info("No offline messages found in Redis for user {}", userId);
            }
        } catch (Exception e) {
            log.error("Failed to retrieve offline messages from Redis: User ID={}", userId, e);
        }
        return offlineMessages;
    }

    /**
     * Delete offline messages from Redis
     * @param userId User ID
     */
    private void deleteOfflineMessages(Integer userId) {
         try {
             String key = UNREAD_COUNT_KEY_PREFIX + "offline:" + userId;
             redisTemplate.delete(key);
             log.info("Successfully deleted offline messages from Redis: User ID={}", userId);
         } catch (Exception e) {
             log.error("Failed to delete offline messages from Redis: User ID={}", userId, e);
         }
    }
}